---
title: HTML
description: Convert Plate content to HTML and vice-versa.
toc: true
---

This guide covers converting Plate editor content to HTML (`serializeHtml`) and parsing HTML back into Plate's format (`editor.api.html.deserialize`).

<ComponentPreview name="html-demo" />

## Kit Usage

<Steps>

### Installation

The fastest way to enable HTML serialization is with the `BaseEditorKit`, which includes pre-configured base plugins that support HTML conversion for most common elements and marks.

<ComponentSource name="editor-base-kit" />

### Add Kit

```tsx
import { createSlateEditor } from 'platejs';
import { serializeHtml } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';

const editor = createSlateEditor({
  plugins: BaseEditorKit,
  value: [
    { type: 'h1', children: [{ text: 'Hello World' }] },
    { type: 'p', children: [{ text: 'This content will be serialized to HTML.' }] },
  ],
});

// Serialize to HTML
const html = await serializeHtml(editor);
```

### Example

See a complete server-side HTML generation example:

<ComponentSource name="slate-to-html" />

</Steps>

## Plate to HTML

Convert Plate editor content (Plate nodes) into an HTML string. This is often done server-side.

[View Server-Side Example](/docs/examples/slate-to-html)

<Callout type="warning" title="Key Server-Side Constraint">
  When using `serializeHtml` or other Plate utilities in a server environment (Node.js, RSC), you **must not** import from `/react` subpaths of any `platejs*` package. Always use the base imports (e.g., `@platejs/basic-nodes` instead of `@platejs/basic-nodes/react`).

  This means you should use `createSlateEditor` from `platejs` for server-side editor instances, not `usePlateEditor` or `createPlateEditor` from `platejs/react`.
</Callout>

<Steps>

### Basic Usage

Provide a server-side editor instance and configure your Plate components during editor creation.

```tsx title="lib/generate-html.ts"
import { createSlateEditor } from 'platejs';
import { serializeHtml } from 'platejs/static'; // Static import
// Import base plugins (NOT from /react paths)
import { BaseHeadingPlugin } from '@platejs/basic-nodes';
// Import your STATIC components for rendering
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
import { HeadingElementStatic } from '@/components/ui/heading-node-static';
// For a styled static output, you might use a wrapper like EditorStatic
import { EditorStatic } from '@/components/ui/editor-static';

// Map plugin keys to their STATIC rendering components
const components = {
  p: ParagraphElementStatic, // 'p' is the default key for paragraphs
  h1: HeadingElementStatic,
  // ... add mappings for all your elements and marks
};

// Create a server-side editor instance with components
const editor = createSlateEditor({
  plugins: [
    BaseHeadingPlugin,   // Base plugin for headings
    // ... add all other base plugins relevant to your content
  ],
  components,
});

async function getMyHtml() {
  // Example: set some content on the server-side editor
  editor.children = [
    { type: 'h1', children: [{text: 'My Title'}] },
    { type: 'p', children: [{text: 'My content.'}] }
  ];

  const html = await serializeHtml(editor, {
    // Optional: Use a custom wrapper like EditorStatic for styling
    // editorComponent: EditorStatic,
    // props: { variant: 'none', className: 'p-4 m-4 border' },
  });

  return html;
}
```

### Styling Serialized HTML

`serializeHtml` returns only the HTML for the editor content itself. If you use styled components (like `EditorStatic` or custom static components with specific classes), you must ensure the necessary CSS is available in the final context where the HTML will be displayed.

This often means wrapping the serialized HTML in a full HTML document that includes your stylesheets:

```tsx title="lib/generate-full-html-document.ts"
// ... (previous setup from generate-html.ts)

async function getFullHtmlDocument() {
  const editorHtmlContent = await getMyHtml(); // From previous example

  const fullHtml = `<!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <link rel="stylesheet" href="/path/to/your-global-styles.css" />
      <link rel="stylesheet" href="/path/to/tailwind-or-component-styles.css" />
      <title>Serialized Content</title>
    </head>
    <body>
      <div class="my-document-wrapper prose dark:prose-invert">
        ${editorHtmlContent}
      </div>
    </body>
  </html>`;
  return fullHtml;
}
```

<Callout type="info" title="Static Output Only">
  The serialization process converts Plate nodes to static HTML. Interactive features (React event handlers, client-side hooks) or components relying on browser APIs will not function in the serialized output.
</Callout>

### Using Static Components

For server-side serialization, you **must** use static versions of your components (no client-only code, no React hooks like `useEffect` or `useState`).

Refer to the [Static Rendering Guide](/docs/static) for detailed instructions on creating server-safe static components for your Plate elements and marks.

```tsx title="components/ui/paragraph-node-static.tsx"
import React from 'react';
import type { SlateElementProps } from 'platejs/static';

// Example static paragraph component
export function ParagraphElementStatic(props: SlateElementProps) {
  return (
    <SlateElement {...props} className={cn('m-0 px-0 py-1')}>
      {props.children}
    </SlateElement>
  );
}
```

</Steps>

---

## HTML to Plate

The HTML deserializer allows you to convert HTML content (strings or DOM elements) back into Plate format. This supports round-trip conversion, preserving structure, formatting, and attributes where corresponding plugin rules exist.

<Steps>

### Basic Usage

Use `editor.api.html.deserialize` within a client-side Plate editor context.

```tsx title="components/my-html-importer.tsx"
import { PlateEditor, usePlateEditor } from 'platejs/react'; // React-specific imports for client-side
// Import ALL Plate plugins needed to represent the HTML content
import { HeadingPlugin } from '@platejs/basic-nodes/react';
// ... and so on for bold, italic, tables, lists, etc.

function MyHtmlImporter({ htmlString }: { htmlString: string }) {
  const editor = usePlateEditor({
    plugins: [
      HeadingPlugin,     // For <h1>, <h2>, etc.
      // ... include all plugins corresponding to the HTML you expect to parse
    ],
  });

  const handleImport = () => {
    const slateValue = editor.api.html.deserialize(htmlString);
    editor.tf.setValue(slateValue);
  };

  // ... render your editor and a button to trigger handleImport ...
  return <button onClick={handleImport}>Import HTML</button>;
}
```

<Callout type="warning" title="Client-Side Operation">
  HTML deserialization using `editor.api.html.deserialize` is typically a client-side operation as it interacts with a live Plate editor instance configured with React components and plugins.
</Callout>

### Plugin Deserialization Rules Overview

Each Plate plugin can define rules for how it interprets specific HTML tags, styles, and attributes during deserialization. Below is a summary of common HTML structures and the Plate plugins typically responsible for them.

| HTML Element / Style                                       | Plate Plugin (Typical)  | Notes                                                                    |
| :--------------------------------------------------------- | :---------------------- | :----------------------------------------------------------------------- |
| `<strong>`, `<b>`, `font-weight: 600,700,bold`          | [`BoldPlugin`](/docs/bold)            | Converts to `bold: true` mark.                                           |
| `<em>`, `<i>`, `font-style: italic`                      | [`ItalicPlugin`](/docs/italic)          | Converts to `italic: true` mark.                                         |
| `<u>`, `text-decoration: underline`                       | [`UnderlinePlugin`](/docs/underline)       | Converts to `underline: true` mark.                                      |
| `<s>`, `<del>`, `<strike>`, `text-decoration: line-through` | [`StrikethroughPlugin`](/docs/strikethrough)   | Converts to `strikethrough: true` mark.                                  |
| `<sub>`, `vertical-align: sub`                            | [`SubscriptPlugin`](/docs/subscript)       | Converts to `subscript: true` mark.                                      |
| `<sup>`, `vertical-align: super`                           | [`SuperscriptPlugin`](/docs/superscript)     | Converts to `superscript: true` mark.                                    |
| `<code>` (not in `<pre>`), `font-family: Consolas`         | [`CodePlugin`](/docs/code)            | Converts to `code: true` mark (inline code).                             |
| `<kbd>`                                                    | [`KbdPlugin`](/docs/kbd)             | Converts to `kbd: true` mark.                                            |
| `<p>`                                                      | [`ParagraphPlugin`](/docs/basic-blocks)       | Converts to paragraph element.                                           |
| `<h1>` - `<h6>`                                            | [`HeadingPlugin`](/docs/heading)         | Converts to corresponding heading elements (`h1` - `h6`).                |
| `<ul>`                                                     | [`ListPlugin` (classic)](/docs/list-classic)            | Converts to unordered list (`ul` type). Items become `li`.               |
| `<ol>`                                                     | [`ListPlugin` (classic)](/docs/list-classic)            | Converts to ordered list (`ol` type). Items become `li`.                 |
| `<li>` (within `<ul>` or `<ol>`)                           | [`ListPlugin` (classic)](/docs/list-classic)            | Converts to list item (`li` type), with `lic` (list item content) child. |
| `<li>` (with `aria-level` for indent)                      | [`ListPlugin`](/docs/list)      | Converts to paragraph with `indent` and `listStyleType` props.           |
| `<blockquote>`                                             | [`BlockquotePlugin`](/docs/blockquote)      | Converts to blockquote element.                                          |
| `<pre>` (often with `<code>` inside)                       | [`CodeBlockPlugin`](/docs/code-block)       | Converts to `code_block` element. Content split into `code_line`.        |
| `<hr>`                                                     | [`HorizontalRulePlugin`](/docs/horizontal-rule)  | Converts to horizontal rule element.                                     |
| `<a>`                                                      | [`LinkPlugin`](/docs/link)            | Converts to link element (`a` type) with `url` property.                 |
| `<img>`                                                    | [`ImagePlugin`](/docs/media)           | Converts to image element (`img` type) with `url` property.              |
| `<iframe>`                                                 | [`MediaEmbedPlugin`](/docs/media)      | Converts to media embed element, attempting to parse URL.                |
| `<table>`                                                  | [`TablePlugin`](/docs/table)           | Converts to `table` element.                                             |
| `<tr>`                                                     | [`TablePlugin`](/docs/table)           | Converts to `tr` (table row) element.                                    |
| `<td>`                                                     | [`TablePlugin`](/docs/table)           | Converts to `td` (table cell) element.                                   |
| `<th>`                                                     | [`TablePlugin`](/docs/table)           | Converts to `th` (table header cell) element.                            |
| `style="background-color: ..."`                          | [`FontColorPlugin`](/docs/font)    | Converts to `backgroundColor` mark. (Plugin name might seem inverse) |
| `style="color: ..."`                                     | [`FontColorPlugin`](/docs/font)       | Converts to `color` mark.                                                |
| `style="font-family: ..."`                               | [`FontFamilyPlugin`](/docs/font)      | Converts to `fontFamily` mark.                                           |
| `style="font-size: ..."`                                 | [`FontSizePlugin`](/docs/font)        | Converts to `fontSize` mark.                                             |
| `style="font-weight: ..."` (other than bold values)      | [`FontWeightPlugin`](/docs/font)      | Converts to `fontWeight` mark for non-standard bold values.              |
| `<mark>`                                                   | [`HighlightPlugin`](/docs/highlight)       | Converts to `highlight: true` mark.                                      |
| `style="text-align: ..."`                                | [`TextAlignPlugin`](/docs/text-align)           | Sets `align` property on block elements.                                 |
| `style="line-height: ..."`                               | [`LineHeightPlugin`](/docs/line-height)      | Sets `lineHeight` property on block elements.                            |

<Callout type="note" title="Plugin Configuration">
  The exact Plate type (e.g., `ParagraphPlugin.key` vs. `'p'`) depends on how plugins are configured. The table shows typical associations. Ensure the corresponding Plate plugins are included in your editor for these rules to apply.
</Callout>

### Deserialization Properties in Plugins

Plugins can define how they handle HTML deserialization using properties within their `parsers.html.deserializer` configuration:

-   **`parse`**: A function `({ editor, element, getOptions, ... }) => Partial<SlateNode>` that takes an HTML element and returns a partial Plate node. This is where the main conversion logic resides.
-   **`query`**: An optional function `({ element, getOptions }) => boolean` that determines if the deserializer rule should even be considered for the current HTML element.
-   **`rules`**: An array of rule objects, each defining conditions for matching an HTML element:
    -   `validNodeName`: String or array of strings for matching HTML tag names (e.g., `'P'`, `['STRONG', 'B']`).
    -   `validAttribute`: Object or array of objects specifying required attribute names and/or values (e.g., `{ align: ['left', 'center'] }`).
    -   `validClassName`: String or array of strings for matching CSS class names.
    -   `validStyle`: Object or array of objects specifying required CSS style properties and/or values (e.g., `{ fontWeight: ['600', '700', 'bold'] }`).
-   **`isElement`**: Boolean, `true` if the plugin deserializes an HTML element into a Plate Element node.
-   **`isLeaf`**: Boolean, `true` if the plugin deserializes an HTML element or style into a Plate Leaf (mark) on a Text node.
-   **`attributeNames`**: Array of HTML attribute names whose values should be preserved on the `node.attributes` property of the resulting Plate node.
-   **`withoutChildren`**: Boolean, if `true`, child nodes of the HTML element are not processed by `convertHtmlAttributes`.

### Customizing Deserialization Behavior

You can extend a plugin to modify its HTML parsing logic. This is useful for supporting non-standard HTML attributes or structures.

```tsx title="lib/custom-code-block-plugin.ts"
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { CodeLinePlugin } from '@platejs/code-block'; // Base if needed

const MyCustomCodeBlockPlugin = CodeBlockPlugin.configure({
  parsers: {
    html: {
      deserializer: {
        // Inherit most rules and properties, then override or add
      o  ...CdeBlockPlugin.parsers.html.deserializer,
        parse: ({ element, editor }) => { // editor might be needed for getType
          const language = element.getAttribute('data-custom-lang') || element.className.match(/language-(?<lang>[^\s]+)/)?.groups?.lang;
          const textContent = element.textContent || '';
          const lines = textContent.split('\n');

          return {
            type: CodeBlockPlugin.key, // Or editor.getType(CodeBlockPlugin.key)
            lang: language,
            code: textContent, // Example: store full code string
            children: lines.map((line) => ({
              type: editor.getType(CodeLinePlugin.key),
              children: [{ text: line }],
            })),
          };
        },
        rules: [
          // Inherit existing rules if desired
          ...(CodeBlockPlugin.parsers.html.deserializer.rules || []),
          // Add a new rule to match based on a custom attribute
          { validAttribute: { 'data-custom-lang': true } },
        ],
      },
    },
  },
});

// Then use MyCustomCodeBlockPlugin in your editor configuration.
```
This example customizes `CodeBlockPlugin` to look for a `data-custom-lang` attribute or a `language-*` class for determining the code language.

### Advanced Deserialization Example (`ListPlugin`)

The `ListPlugin` demonstrates a more complex deserialization scenario where it transforms HTML list structures (`<li>` elements) into indented paragraphs within Plate, using `aria-level` to determine indentation.

Here's a conceptual look at its deserialization logic:

```ts
// Simplified concept from ListPlugin
export const ListPluginConfig = {
  // ... other configurations ...
  parsers: {
    html: {
      deserializer: {
        isElement: true,
        // query: ({ element }) => hasListAncestor(element), // Example condition
        parse: ({ editor, element }) => ({
          type: 'p', // Converts <li> to <p>
          indent: Number(element.getAttribute('aria-level') || '1'),
          listStyleType: element.style.listStyleType || undefined,
          // Children are processed by Plate's default deserializer after this node is created
        }),
        rules: [
          { validNodeName: 'LI' }, // Only applies to <li> elements
        ],
      },
    },
  },
};
```
This illustrates how a plugin can completely reinterpret HTML structures into a different Plate representation.

</Steps>

## API Reference

### `serializeHtml(editor, options)`

Converts Plate nodes from `editor.children` (or a provided `value`) into an HTML string. This function is typically used server-side.

<API name="serializeHtml">
<APIParameters>
  <APIItem name="editor" type="PlateEditor">
    A server-side Plate editor instance, created via `createSlateEditor` with components configured.
  </APIItem>
  <APIItem name="options" type="SerializeHtmlOptions">
    Options for serialization.
  </APIItem>
</APIParameters>
<APIOptions type="SerializeHtmlOptions<P = PlateStaticProps>">
  <APIItem name="editorComponent" type="React.ComponentType<P>" optional>
    A React component to wrap the entire editor content during static rendering. Defaults to `PlateStatic`.
    The component receives `editor`, `value`, and any `props` passed here.
  </APIItem>
  <APIItem name="props" type="Partial<P>" optional>
    Props to pass to the `editorComponent`. `P` defaults to `PlateStaticProps`.
  </APIItem>
  <APIItem name="value" type="Descendant[]" optional>
    Plate nodes to serialize. If not provided, `editor.children` will be used.
  </APIItem>
  <APIItem name="preserveClassNames" type="string[] | null" optional>
    An array of class name prefixes to preserve if `stripClassNames` is true. `null` preserves all if `stripClassNames` is true. Default: `['slate-', 'line-clamp']`.
  </APIItem>
  <APIItem name="stripClassNames" type="boolean" optional>
    If `true`, removes all class names from the output HTML except those whose prefixes are listed in `preserveClassNames`. Default: `true`.
  </APIItem>
  <APIItem name="stripDataAttributes" type="boolean" optional>
    If `true`, removes all `data-*` attributes from the output HTML. Default: `true`.
  </APIItem>
</APIOptions>
<APIReturns>
  <APIItem type="Promise<string>">
    A promise that resolves to the serialized HTML string.
  </APIItem>
</APIReturns>
</API>

---

### `api.html.deserialize(options)`

Parses an HTML string or `HTMLElement` into a Plate `Value` (an array of `Descendant` nodes). This is typically used on the client-side with a fully configured Plate editor.

<API name="deserializeHtml">
<APIParameters>
  <APIItem name="editor" type="PlateEditor">
    The client-side Plate editor instance.
  </APIItem>
  <APIItem name="options" type="DeserializeHtmlOptions">
    Options for deserialization.
  </APIItem>
</APIParameters>
<APIOptions type="DeserializeHtmlOptions">
  <APIItem name="element" type="HTMLElement | string">
    The HTML string or `HTMLElement` to deserialize.
  </APIItem>
  <APIItem name="collapseWhiteSpace" type="boolean" optional>
    If `true` (default), collapses whitespace from text nodes similarly to how browsers treat whitespace in HTML. Set to `false` to preserve all whitespace. Default: `true`.
  </APIItem>
  <APIItem name="stripWhitespace" type="boolean" optional>
    **Deprecated.** Use `collapseWhiteSpace`. If `true`, leading/trailing whitespace is trimmed and sequences of whitespace are collapsed. Default: `true`.
  </APIItem>
</APIOptions>
<APIReturns>
  <APIItem type="Descendant[]">
    The deserialized Plate `Value`.
  </APIItem>
</APIReturns>
</API>

## Next Steps

-   Explore the [Static Rendering guide](/docs/static) for creating server-safe components.
-   Review individual plugin documentation for specific HTML serialization/deserialization capabilities and default rules.
-   See the [Server-Side HTML Generation Example](/docs/examples/slate-to-html).
